import base64

from google.appengine.api import memcache
from google.appengine.ext import db
from google.appengine.ext.webapp import template

# TODO(flamingcow): SDK > 1.4.3 should contain this fix and we can go back to
# the official version.
# from google.appengine.ext import search
from pylib import search


######
# Meta
######

class Setting(db.Model):
  name = db.StringProperty()
  value = db.ByteStringProperty()

  @staticmethod
  def Get(name, default_value_callback):
    """Fetch a setting via cache or datastore, or fetch its default value."""
    client = memcache.Client()
    memcache_key = 'setting-%s' % name
    value = client.get(memcache_key)
    if value:
      return value

    # Cache miss
    settings = Setting.all().filter('name =', name).fetch(1)
    if settings:
      value = settings[0].value
      client.set(memcache_key, value)
      return value 

    # Datastore miss
    value = default_value_callback()
    Setting(name=name, value=value).put()
    client.set(memcache_key, value)
    return value


#########
# Changes
#########

class Change(db.Model):
  target = db.ReferenceProperty()  # Unset if deleted
  old_pb = db.ByteStringProperty()  # Unset if added
  new_pb = db.ByteStringProperty()  # Unset if deleted
  author = db.UserProperty(auto_current_user_add=True,
                           required=True)
  remote_addr = db.StringProperty(required=True)
  changed_at = db.DateTimeProperty(auto_now_add=True,
                                   required=True)

  def __init__(self, changed_people, **kwargs):
    db.Model.__init__(self, **kwargs)
    self._changed_people = changed_people

  def put(self):
    db.Model.put(self)
    for person in self._changed_people:
      changedperson = ChangedPerson(parent=self,
                                    change=self,
                                    person=person)
      changedperson.put()

  def __str__(self):
    values = {
      'change': self,
    }
    if self.old_pb:
      values['old'] = db.model_from_protobuf(self.old_pb)
    if self.new_pb:
      values['new'] = db.model_from_protobuf(self.new_pb)
    return template.render('../templates/change.html', values)


class ChangeTracker(db.Model):
  historical = db.BooleanProperty(default=False,
                                  required=True)

  def put(self, request, changed_people=()):
    is_update = self.is_saved() and not self.historical
    db.Model.put(self)
    self.historical = True
    change = Change(parent=self,
                    target=self,
                    new_pb=db.model_to_protobuf(self).Encode(),
                    changed_people=changed_people,
                    remote_addr=request.remote_addr)
    if is_update:
      original = db.Model.get(self.key())
      original.historical = True
      change.old_pb = db.model_to_protobuf(original).Encode()
    change.put()

  def delete(self, request, changed_people=()):
    self.historical = True
    change = Change(parent=self,
                    target=self,
                    old_pb=db.model_to_protobuf(self).Encode(),
                    changed_people=changed_people,
                    remote_addr=request.remote_addr)
    change.put()
    db.Model.delete(self)


################
# People (nodes)
################

class Person(ChangeTracker):
  birth_year = db.IntegerProperty()
  birth_month = db.IntegerProperty()
  birth_day = db.IntegerProperty()

  death_year = db.IntegerProperty()
  death_month = db.IntegerProperty()
  death_day = db.IntegerProperty()

  biological_gender = db.StringProperty(choices=('Male', 'Female', 'Intersex'))

  def put(self, **kwargs):
    ChangeTracker.put(self,
                      changed_people=(self,),
                      **kwargs)

  def delete(self, **kwargs):
    for identity in self.identity_set.ancestor(self):
      identity.delete(**kwargs)
    ChangeTracker.delete(self,
                         changed_people=(self,),
                         **kwargs)

  def __str__(self):
    values = {
      'person': self,
      'proto': base64.b64encode(db.model_to_protobuf(self).Encode()),
    }
    return template.render('../templates/person.html', values)


class Identity(ChangeTracker, search.SearchableModel):
  person = db.ReferenceProperty(reference_class=Person,
                                required=True)
  name = db.StringProperty(required=True)

  def put(self, **kwargs):
    ChangeTracker.put(self,
                      changed_people=(self.person,),
                      **kwargs)

  def delete(self, **kwargs):
    ChangeTracker.delete(self,
                         changed_people=(self.person,),
                         **kwargs)

  def __str__(self):
    values = {
      'identity': self,
      'proto': base64.b64encode(db.model_to_protobuf(self).Encode()),
    }
    return template.render('../templates/identity.html', values)


class ChangedPerson(db.Model):
  change = db.ReferenceProperty(reference_class=Change)
  person = db.ReferenceProperty(reference_class=Person)


#######################
# Relationships (edges)
#######################

class Relationship(ChangeTracker):
  source = db.ReferenceProperty(reference_class=Person,
                                collection_name='source_set',
                                required=True)
  destination = db.ReferenceProperty(reference_class=Person,
                                     required=True)

  def put(self, **kwargs):
    ChangeTracker.put(self,
                      changed_people=(self.source, self.destination),
                      **kwargs)

  def delete(self, **kwargs):
    ChangeTracker.delete(self,
                         changed_people=(self.source, self.destination),
                         **kwargs)


class LegalRelationship(Relationship):
  start = db.DateProperty()
  end = db.DateProperty()


class BiologicalRelationship(Relationship):
  pass


class BiologicalParent(BiologicalRelationship):
  forward = 'a biological parent of'
  reverse = 'a biological child of'


class LegalParent(LegalRelationship):
  forward = 'a legal parent of'
  reverse = 'a legal child of'


class Marriage(LegalRelationship):
  forward = 'married to'
  reverse = 'married to'
